﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Mvc;

using AMC.SubstitutionCache.Events;
using AMC.SubstitutionCache.Models;
using AMC.SubstitutionCache.Rendering;

using Orchard.Logging;

namespace AMC.SubstitutionCache.Services
{
    /// <summary>
    /// Processes CacheItem.Output for replacement tokens, renders replacement, and replaces token in results
    /// </summary>
    public class SubstitutionProcessorService : ISubstitutionProcessorService
    {
        private static readonly Regex _regex = new Regex(
            Regex.Escape(SubstitutionConstants.BeginSubstitutionTokenStart) +
            "(?<Key>.+?)" +
            Regex.Escape(SubstitutionConstants.SubstitutionTokenTail) +
            "(?<Value>.+?)" +
            Regex.Escape(SubstitutionConstants.EndSubstitutionTokenStart) +
            "\\k<Key>" +
            Regex.Escape(SubstitutionConstants.SubstitutionTokenTail),
            RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);

        private readonly IDictionary<string, string> _regexCache = new Dictionary<string, string>();
        private readonly ISubstitutionShapeRenderer _renderManager;
        private readonly IEnumerable<ISubstitutionEvents> _shapeEventHandlers;

        private IAntiForgeryTokenService _tokenService;

        public SubstitutionProcessorService(
            ISubstitutionShapeRenderer renderManager,
            IEnumerable<ISubstitutionEvents> shapeEventHandlers,
            IAntiForgeryTokenService tokenService)
        {
            _renderManager = renderManager;
            _shapeEventHandlers = shapeEventHandlers;
            _tokenService = tokenService;

            Logger = NullLogger.Instance;
        }

        protected ILogger Logger { get; set; }

        /// <summary>
        /// Processes Cache Item Output for replacement tokens, renders replacement, and replaces token in results
        /// </summary>
        public string Process(CacheItem cacheItem, Guid? requestId, ControllerContext controllerContext)
        {
            // assume if there are no replacement matchContext, that there is nothing to replace
            // if we are wrong the output should still work, it will just have previously cached output with no replacements
            if (cacheItem.SubstitutionContexts == null || cacheItem.SubstitutionContexts.Length == 0)
            {
                Logger.Debug("No Substitutions found. Using cacheItem Output. CacheKey: {0}, RequestId: {1}", cacheItem.InvariantCacheKey, requestId);

                return cacheItem.Output;
            }

            Logger.Debug("Starting Substitutions Processing. CacheKey: {0}, RequestId: {1}", cacheItem.InvariantCacheKey, requestId);

            // assume this is getting called once per request, not sure if we can guarantee that
            // matching keys across pages match, so only cache matches for a request
            _regexCache.Clear();

            var watch = new Stopwatch();
            watch.Start();

            // convert to dict for speed
            var replacementContexts = cacheItem.SubstitutionContexts.ToDictionary(r => r.Key);

            // TODO: perf enhancement: this could probably be replaced with a stream reader / decorator
            // and avoid extra copies of the outputs during replacement

            try
            {
                var output = _regex.Replace(cacheItem.Output,  m => MatchEvaluator(
                    new MatchContext
                    {
                        Match = m,
                        Contexts = replacementContexts,
                        RequestId = requestId,
                        ControllerContext = controllerContext,
                        CacheKey = cacheItem.InvariantCacheKey
                    }));

                output = _tokenService.ReplaceTokensWithBeacons(output);

                Logger.Debug("Finished Substitutions Processing in {2}. CacheKey: {0}, RequestId: {1}", cacheItem.InvariantCacheKey, requestId, watch.Elapsed);

                return output;
            }
            catch (Exception ex)
            {
                Logger.Debug("Finished Substitutions Processing in {2} with error {3}. CacheKey: {0}, RequestId: {1}", cacheItem.InvariantCacheKey, watch.Elapsed, ex.Message);
                Logger.Error(ex, "Error while processing cache output");
            }

            return cacheItem.Output;
        }

        private string MatchEvaluator(MatchContext matchContext)
        {
            var key = matchContext.Match.Groups["Key"].Captures[0].Value;
            var value = matchContext.Match.Groups["Value"].Captures[0].Value;
            var matchRequestId = matchContext.RequestId.GetValueOrDefault();
            var debugLogContext = string.Format("CacheKey: {0}, Substitution CacheKey: {1}, RequestId: {2}", matchContext.CacheKey, key, matchRequestId);

            // short circuit re-render if we have already rendered this shape for the request
            if (_regexCache.ContainsKey(key))
            {
                Logger.Debug("Substitution previously rendered. {0}", debugLogContext);

                return _regexCache[key];
            }

            Logger.Debug("Starting Substitution Rendering. {0}", debugLogContext);

            var watch = new Stopwatch();
            watch.Start();

            try
            {
                SubstitutionContext substitutionContext;

                if (!matchContext.Contexts.TryGetValue(key, out substitutionContext))
                    throw new ArgumentException(string.Format("Matching item for key '{0}' could not be found in dictionary.", key), "matchContext");

                // if the current request is the request that the SubstitutionContext was created
                // then just use the current value
                if (substitutionContext.RequestId == matchRequestId)
                {
                    Logger.Debug("Current request matches substitution request. Returning rendered value. {0}", debugLogContext);
                    
                    return value;
                }

                var replacingContext = new SubstitutionReplacingContext
                {
                    SubstitutionContext = substitutionContext
                };

                foreach (var handler in _shapeEventHandlers)
                    handler.Replacing(replacingContext);

                if (!string.IsNullOrWhiteSpace(replacingContext.ShapeOutput))
                {
                    Logger.Debug("Finished Substitution Rendering from ReplacingContext in {2}. {0}", debugLogContext, watch.Elapsed);

                    return replacingContext.ShapeOutput;
                }

                // TODO: possibly move the caching of outputs up a level from the ShapeDriverRenderer to allow all implementations to 
                // get caching rather than requiring them to implement it independently

                var output = _renderManager.GetOutput(substitutionContext, matchContext.ControllerContext);

                _regexCache.Add(key, output);

                var replacedContext = new SubstitutionReplacedContext
                {
                    SubstitutionContext = substitutionContext,
                    ShapeOutput = output
                };

                foreach (var handler in _shapeEventHandlers)
                    handler.Replaced(replacedContext);

                Logger.Debug("Finished Substitution Rendering from SubstitutionProcessorService in {2}. {0}", debugLogContext, watch.Elapsed);

                return replacedContext.ShapeOutput;
            }
            catch (Exception ex)
            {
                Logger.Debug("Finished Substitution Rendering in {2} with error '{3}'. {0}", debugLogContext, watch.Elapsed, ex.Message);

                // if we hit an error log it, and just return the inner value so we at least display something
                Logger.Error(ex, "Error recreating shape value.");
                return string.Empty;
            }
        }

        private class MatchContext
        {
            public Match Match { get; set; }

            public Guid? RequestId { get; set; }

            public IDictionary<string, SubstitutionContext> Contexts { get; set; }

            public ControllerContext ControllerContext { get; set; }

            public string CacheKey { get; set; }
        }
    }
}